/*
 * Author: yezi
 * E-mail: okye328@gmail.com
 * Create time: Jan 12, 2013 8:55:56 PM
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
 */

package com.kanbox.linux.fs.fuse;

import java.io.File;
import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;

import com.alibaba.fastjson.JSONObject;
import com.kanbox.linux.fs.FolderHash;
import com.kanbox.linux.fs.MountPointManager;
import com.kanbox.linux.operation.FileUploader;
import com.kanbox.linux.operation.Information;
import com.kanbox.linux.util.FileUtil;

import fuse.Filesystem3;
import fuse.FuseDirFiller;
import fuse.FuseException;
import fuse.FuseFtype;
import fuse.FuseGetattrSetter;
import fuse.FuseOpenSetter;
import fuse.FuseSizeSetter;
import fuse.FuseStatfsSetter;
import fuse.XattrLister;
import fuse.XattrSupport;

public class KanboxFuseFileSystem implements Filesystem3, XattrSupport {

	private static final int BLOCKSIZE = 1024;

	private File dataRoot = new File(MountPointManager.getDataPath());
	private Information kanboxInfo = new Information();

	public KanboxFuseFileSystem() {
		Thread refresh = new Thread(new Runnable() {

			@Override
			public void run() {
				while (true) {
					try {
						Thread.sleep(60000);
					} catch (InterruptedException e) {
					}
					kanboxInfo = new Information();
				}
			}
		});
		refresh.setDaemon(true);
		refresh.start();
	}

	@Override
	public int statfs(FuseStatfsSetter statfsSetter) throws FuseException {
		int blockSize = BLOCKSIZE;
		int blocks = (int) (kanboxInfo.getTotalSpace() / blockSize);
		int blocksFree = (int) ((kanboxInfo.getTotalSpace() - kanboxInfo
				.getUsedSpace()) / blockSize);
		int blocksAvail = -1; // Inner is blocksFree*20/19, why?
		int files = FileUtil.countFiles(dataRoot);
		int filesFree = blocksFree;
		int namelen = 255;// Length of name, not path?
		// set(int blockSize, int blocks, int blocksFree, int
		// blocksAvail, int files, int filesFree, int namelen);

		statfsSetter.set(blockSize, blocks, blocksFree, blocksAvail, files,
				filesFree, namelen);
		return 0;
	}

	@Override
	public int getattr(String path, FuseGetattrSetter fuseGetattrSetter)
			throws FuseException {
		File file = getActualFile(path);
		if (!file.exists()) {
			throw new FuseException("No Such File")
					.initErrno(FuseException.ENOENT);
		}

		// set(long inode, int mode, int nlink, int uid, int gid, int rdev,
		// long size, long blocks, int atime, int mtime, int ctime)

		Path pf = file.toPath();
		try {
			int time = (int) (file.lastModified() / 1000L);
			int mode = (file.isDirectory() ? FuseFtype.TYPE_DIR
					: FuseFtype.TYPE_FILE) | FileUtil.getPermission(pf);
			int uid = FileUtil.getUID(pf);
			int gid = FileUtil.getGID(pf);
			// long ino = FileUtil.getInode(pf);
			// int nlink = FileUtil.getNLink(pf);
			// int rdev = (int) FileUtil.getRDev(pf);
			// fuseGetattrSetter.set(ino, mode, nlink, uid, gid, rdev,
			// file.length(), (file.length() + BLOCKSIZE - 1) / BLOCKSIZE,
			// time, time, time);
			fuseGetattrSetter.set(0, mode, 1, uid, gid, 0, file.length(),
					(file.length() + BLOCKSIZE - 1) / BLOCKSIZE, time, time,
					time);
		} catch (IOException e) {
			throw new FuseException("Get file attribute error", e)
					.initErrno(FuseException.EACCES);
		}

		return 0;
	}

	@Override
	public int chmod(String path, int mode) throws FuseException {
		File file = getActualFile(path);
		Path p = file.toPath();
		try {
			Files.setPosixFilePermissions(p, FileUtil.getFilePermissions(mode));
		} catch (IOException e) {
			throw new FuseException("Error changing mode", e)
					.initErrno(FuseException.EACCES);
		}
		return 0;
	}

	@Override
	public int chown(String path, int uid, int gid) throws FuseException {
		File file = getActualFile(path);
		Path p = file.toPath();
		try {
			Files.setAttribute(p, "unix:uid", uid);
			Files.setAttribute(p, "unix:gid", gid);
		} catch (IOException e) {
			throw new FuseException("Error changing mode", e)
					.initErrno(FuseException.EACCES);
		}
		return 0;
	}

	/**
	 * flush() called when a file is closed (can be called multiple times for
	 * each dup-ed filehandle), called on every filehandle close
	 */
	@Override
	public int flush(String path, Object fh) throws FuseException {
		// Nothing should be done
		// System.out.println("Flush - Path:" + path + " File:"
		// + ((File) fh).getAbsolutePath());
		return 0;
	}

	/**
	 * fsync() called when file data should be synced (with a flag to sync only
	 * data but not metadata). Synchronize file contents, fh is filehandle
	 * passed from open, isDatasync indicates that only the user data should be
	 * flushed, not the meta data.
	 */
	@Override
	public int fsync(String path, Object fh, boolean isDataSync)
			throws FuseException {
		// When the file is wrote and saved, this method will be called

		File file = getActualFile(path);
		final FileUploader fu = new FileUploader(file);
		final FolderHash folerHash = (FolderHash) fh;
		new Thread(new Runnable() {

			@Override
			public void run() {
				JSONObject jo = fu.upload();

				// TODO handle
				System.out.println(jo);

				folerHash.refresh();
			}
		}).start();

		return 0;
	}

	/**
	 * - release() is called when an open file has: 1) all file descriptors
	 * closed, 2) all memory mappings unmapped. This call need only be
	 * implemented if this information is required.
	 */
	@Override
	public int release(String path, Object fh, int flags) throws FuseException {
		// When the file is closed(either read or write), this method would
		// be called. So we can't indicate what to do.

		return 0;
	}

	@Override
	public int getdir(String path, FuseDirFiller filler) throws FuseException {
		File file = getActualFile(path);
		File[] files = file.listFiles();
		for (File f : files) {
			Path p = f.toPath();
			// add(String name, long inode, int mode)
			try {
				filler.add(f.getName(), 0, FileUtil.getPermission(p));
			} catch (IOException e) {
				throw new FuseException("Get file attribute error", e)
						.initErrno(FuseException.EACCES);
			}
		}
		return 0;
	}

	@Override
	public int link(String from, String to) throws FuseException {
		// Not to link inner?
		return 0;
	}

	@Override
	public int readlink(String path, CharBuffer link) throws FuseException {
		// No inner link?
		return 0;
	}

	@Override
	public int symlink(String from, String to) throws FuseException {
		// Not support?
		return 0;
	}

	@Override
	public int mkdir(String path, int mode) throws FuseException {
		File file = getActualFile(path);
		file.mkdirs();
		Path p = file.toPath();
		try {
			Files.setPosixFilePermissions(p, FileUtil.getFilePermissions(mode));
		} catch (IOException e) {
			throw new FuseException("Error creating dir", e)
					.initErrno(FuseException.EACCES);
		}
		// TODO call the kanbox api.
		return 0;
	}

	@Override
	public int mknod(String path, int mode, int rdev) throws FuseException {
		Path p = getActualFile(path).toPath();
		try {
			Files.createFile(p);
			Files.setPosixFilePermissions(p, FileUtil.getFilePermissions(mode));
		} catch (IOException e) {
			throw new FuseException("Error making node.", e)
					.initErrno(FuseException.EACCES);
		}
		// TODO Call the kanbox api?
		return 0;
	}

	/**
	 * Flag: O_RDONLY, O_WRONLY, O_RDWR
	 */
	@Override
	public int open(String path, int flags, FuseOpenSetter openSetter)
			throws FuseException {
		File file = getActualFile(path);
		String parent = file.getParent();
		parent = MountPointManager.localToRemoteDataPath(parent);

		FolderHash folderHash = new FolderHash(parent);

		openSetter.setDirectIO(true);
		openSetter.setFh(folderHash);
		openSetter.setKeepCache(true);
		return 0;
	}

	@Override
	public int read(String path, Object fh, ByteBuffer buf, long offset)
			throws FuseException {
		Path p = getActualFile(path).toPath();
		SeekableByteChannel channel = null;
		try {
			channel = Files.newByteChannel(p, StandardOpenOption.READ)
					.position(offset);
			channel.read(buf);
			if (channel != null) {
				channel.close();
			}
		} catch (IOException e) {
			throw new FuseException("Failed to read file", e)
					.initErrno(FuseException.EACCES);
		} finally {
			if (channel != null) {
				try {
					channel.close();
				} catch (IOException e) {
					throw new FuseException("Error closing file", e)
							.initErrno(FuseException.EACCES);
				}
			}
		}
		return 0;
	}

	/**
	 * isWritepage indicates that write was caused by a writepage
	 */
	@Override
	public int write(String path, Object fh, boolean isWritePage,
			ByteBuffer buf, long offset) throws FuseException {
		Path p = getActualFile(path).toPath();
		SeekableByteChannel channel = null;
		try {
			channel = Files.newByteChannel(p, StandardOpenOption.WRITE,
					StandardOpenOption.CREATE);
			channel.position(offset);
			channel.write(buf);
		} catch (IOException e) {
			throw new FuseException("Error writing file", e)
					.initErrno(FuseException.EACCES);
		} finally {
			if (channel != null) {
				try {
					channel.close();
				} catch (IOException e) {
					throw new FuseException("Error closing file", e)
							.initErrno(FuseException.EACCES);
				}
			}
		}
		// Call kanbox api? NO! This should be call in flush or release
		return 0;
	}

	@Override
	public int rename(String path, String to) throws FuseException {
		Path p = getActualFile(path).toPath();
		Path tp = getActualFile(to).toPath();
		try {
			Files.move(p, tp, StandardCopyOption.ATOMIC_MOVE);
		} catch (IOException e) {
			throw new FuseException("Error moving file", e)
					.initErrno(FuseException.EACCES);
		}
		// TODO call kanbox API
		return 0;
	}

	@Override
	public int rmdir(String path) throws FuseException {
		File file = getActualFile(path);
		FileUtil.deleteAll(file);
		// TODO call kanbox API
		return 0;
	}

	@Override
	public int truncate(String path, long size) throws FuseException {
		// TODO Auto-generated method stub
		return 0;
	}

	@Override
	public int unlink(String path) throws FuseException {
		// It's actually remove file
		File file = getActualFile(path);
		file.delete();
		// TODO Kanbox remove file
		return 0;
	}

	@Override
	public int utime(String path, int atime, int mtime) throws FuseException {
		// Not support
		return 0;
	}

	private File getActualFile(String path) {
		return new File(MountPointManager.remoteToLocalDataPath(path));
	}

	/**
	 * This method will be called to get the value of the extended attribute
	 * 
	 * @param path
	 *            the path to file or directory containing extended attribute
	 * @param name
	 *            the name of the extended attribute
	 * @param dst
	 *            a ByteBuffer that should be filled with the value of the
	 *            extended attribute
	 * @return 0 if Ok or errno when error
	 * @throws FuseException
	 *             an alternative to returning errno is to throw this exception
	 *             with errno initialized
	 * @throws BufferOverflowException
	 *             should be thrown to indicate that the given <code>dst</code>
	 *             ByteBuffer is not large enough to hold the attribute's value.
	 *             After that <code>getxattr()</code> method will be called
	 *             again with a larger buffer.
	 */
	@Override
	public int getxattr(String path, String name, ByteBuffer dst)
			throws FuseException, BufferOverflowException {
		return 0;
	}

	/**
	 * This method can be called to query for the size of the extended attribute
	 * 
	 * @param path
	 *            the path to file or directory containing extended attribute
	 * @param name
	 *            the name of the extended attribute
	 * @param sizeSetter
	 *            a callback interface that should be used to set the
	 *            attribute's size
	 * @return 0 if Ok or errno when error
	 * @throws FuseException
	 *             an alternative to returning errno is to throw this exception
	 *             with errno initialized
	 */
	@Override
	public int getxattrsize(String path, String name, FuseSizeSetter sizeSetter)
			throws FuseException {
		return 0;
	}

	/**
	 * This method will be called to get the list of extended attribute names
	 * 
	 * @param path
	 *            the path to file or directory containing extended attributes
	 * @param lister
	 *            a callback interface that should be used to list the attribute
	 *            names
	 * @return 0 if Ok or errno when error
	 * @throws FuseException
	 *             an alternative to returning errno is to throw this exception
	 *             with errno initialized
	 */
	@Override
	public int listxattr(String path, XattrLister lister) throws FuseException {
		return 0;
	}

	/**
	 * This method will be called to remove the extended attribute
	 * 
	 * @param path
	 *            the path to file or directory containing extended attributes
	 * @param name
	 *            the name of the extended attribute
	 * @return 0 if Ok or errno when error
	 * @throws FuseException
	 *             an alternative to returning errno is to throw this exception
	 *             with errno initialized
	 */
	@Override
	public int removexattr(String path, String name) throws FuseException {
		return 0;
	}

	/**
	 * This method will be called to set the value of an extended attribute
	 * 
	 * @param path
	 *            the path to file or directory containing extended attributes
	 * @param name
	 *            the name of the extended attribute
	 * @param value
	 *            the value of the extended attribute
	 * @param flags
	 *            parameter can be used to refine the semantics of the
	 *            operation.
	 *            <p>
	 *            <code>XATTR_CREATE</code> specifies a pure create, which
	 *            should fail with <code>Errno.EEXIST</code> if the named
	 *            attribute exists already.
	 *            <p>
	 *            <code>XATTR_REPLACE</code> specifies a pure replace operation,
	 *            which should fail with <code>Errno.ENOATTR</code> if the named
	 *            attribute does not already exist.
	 *            <p>
	 *            By default (no flags), the extended attribute will be created
	 *            if need be, or will simply replace the value if the attribute
	 *            exists.
	 * @return 0 if Ok or errno when error
	 * @throws FuseException
	 *             an alternative to returning errno is to throw this exception
	 *             with errno initialized
	 */
	@Override
	public int setxattr(String path, String name, ByteBuffer value, int flags)
			throws FuseException {
		return 0;
	}

	public static void main(String[] args) {
		File file = new File(
				MountPointManager.remoteToLocalDataPath("/test/abc.txt"));
		String parent = file.getParent();
		parent = MountPointManager.localToRemoteDataPath(parent);
		System.out.println(parent);
		// FolderHash fh = new FolderHash(parent);
	}
}
